﻿using System;
using System.Configuration;
using System.Net;
using System.Net.Security;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Description;
using Cubicle.Core.Interfaces;

namespace Cubicle.Core.Hosting
{
    /// <summary>
    /// The connection manager can be used to establish connections to Linq DataContexts or
    /// generic web-services
    /// </summary>
    public class ConnectionManager
    {
        // Nlog
        private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();

        #region Generic Web Service Connections

        private static readonly TimeSpan ReceiveTimeout = new TimeSpan(0,10,0);
        private static readonly TimeSpan SendTimeout = new TimeSpan(0, 10, 0);
        private static readonly TimeSpan OpenTimeout = new TimeSpan(0, 0, 15);
        private static readonly TimeSpan CloseTimeout = new TimeSpan(0, 0, 15);
        private const int MaxBufferSize = 1024 * 1024 * 10;             // 10Mb
        private const int MaxReceivedMessageSize = 1024*1024*10;    // 10Mb
        private const int MaxArrayLength = 1024*1024*10;            // 10Mb

        public static IEngine CreateCortexClient(string address)
        {
            var factory = CreateCortexChannelFactory(address);
            return factory.CreateChannel();
        }

        public static ChannelFactory<IEngine> CreateCortexChannelFactory(string address)
        {
            System.ServiceModel.Channels.Binding binding;
            if (address.StartsWith(@"http://"))
            {
                var bindingHttp = GetHttpBinding(SecurityMode.None);
                binding = bindingHttp;
            }
            else if (address.StartsWith(@"net.pipe://"))
                binding = GetPipeBinding(NetNamedPipeSecurityMode.None);
            else if (address.StartsWith(@"net.tcp://"))
                binding = GetTcpBinding(SecurityMode.None);
            else return null;

            var endpoint = new EndpointAddress(address);
            var factory = new ChannelFactory<IEngine>(binding, endpoint);
            AttachDataContractResolver(factory.Endpoint);
            return factory;
        }

        //public static DuplexChannelFactory<ISubscribeContract> CreateEventChannelFactory(
        //    string eventServiceAddress,
        //    IEventContractCallback callbackHandler,
        //    string httpClientServerName = null,
        //    int? httpPort = null,
        //    string identifier = null)
        //{
        //    System.ServiceModel.Channels.Binding binding;
        //    if (eventServiceAddress.StartsWith(@"http://"))
        //    {
        //        if (String.IsNullOrEmpty(httpClientServerName) || httpPort == null) return null;
        //        var bindingHttp = GetDualHttpBinding(WSDualHttpSecurityMode.None);
        //        identifier = identifier ?? new Guid().ToString();
        //        var uriBuilder = new UriBuilder(Uri.UriSchemeHttp, httpClientServerName, (int)httpPort, identifier);
        //        var clientCallbackEndpoint = uriBuilder.Uri;
        //        bindingHttp.ClientBaseAddress = clientCallbackEndpoint;
        //        binding = bindingHttp;
        //    }
        //    else if (eventServiceAddress.StartsWith(@"net.pipe://"))
        //        binding = GetPipeBinding(NetNamedPipeSecurityMode.None);
        //    else if (eventServiceAddress.StartsWith(@"net.tcp://"))
        //        binding = GetTcpBinding(SecurityMode.None);
        //    else return null;

        //    var site = new InstanceContext(null, callbackHandler);
        //    var endpoint = new EndpointAddress(eventServiceAddress);
        //    var factory = new DuplexChannelFactory<ISubscribeContract>(site, binding, endpoint);
        //    AttachDataContractResolver(factory.Endpoint);
        //    return factory;
        //}

        ///// <summary>
        ///// Create a client to the event callback service from the Exocortex
        ///// </summary>
        ///// <param name="eventServiceAddress">The address of the cortex hosting the event register. e.g. http://address:port/Exocortex.svc</param>
        ///// <param name="callbackHandler">The class implementing the IEventContactCallback on the client</param>
        ///// <param name="httpClientServerName">Optional, only required for bindingEnum = BindingEnum.Http. The dns name / ip of the client.</param>
        ///// <param name="httpPort">Optional, only required for bindingEnum = BindingEnum.Http. The client port to open for HTTP callbacks.</param>
        ///// <param name="identifier">Optional, only required for bindingEnum = BindingEnum.Http. A unique identifier for this client (can be anything).</param>
        ///// <returns></returns>
        //public static EventContractClient CreateEventClient(
        //    string eventServiceAddress, 
        //    IEventContractCallback callbackHandler,
        //    string httpClientServerName = null,
        //    int? httpPort = null,
        //    string identifier = null)
        //{
        //    System.ServiceModel.Channels.Binding binding;
        //    if (eventServiceAddress.StartsWith(@"http://"))
        //    {
        //        if (String.IsNullOrEmpty(httpClientServerName) || httpPort == null) return null;
        //        var bindingHttp = GetDualHttpBinding(WSDualHttpSecurityMode.None);
        //        identifier = identifier ?? new Guid().ToString();
        //        var uriBuilder = new UriBuilder(Uri.UriSchemeHttp, httpClientServerName, (int)httpPort, identifier);
        //        var clientCallbackEndpoint = uriBuilder.Uri;
        //        bindingHttp.ClientBaseAddress = clientCallbackEndpoint;
        //        binding = bindingHttp;
        //    }
        //    else if (eventServiceAddress.StartsWith(@"net.pipe://"))
        //        binding = GetPipeBinding(NetNamedPipeSecurityMode.None);
        //    else if (eventServiceAddress.StartsWith(@"net.tcp://"))
        //        binding = GetTcpBinding(SecurityMode.None);
        //    else return null;

        //    var site = new InstanceContext(null, callbackHandler);
        //    var endpoint = new EndpointAddress(eventServiceAddress);
        //    var client = new EventContractClient(site, binding, endpoint);

        //    // Check if the connection can be opened
        //    try
        //    {
        //        client.Open();
        //    } catch (Exception e)
        //    {
        //        _logger.ErrorException(e, "Error establishing connection with event service. " + 
        //            "Ensure that you have privileges to open port {0}. ", httpPort);
        //        if (eventServiceAddress.StartsWith(@"http://"))
        //            _logger.Info(@"Ask your administrator to execute from the commandline: netsh http add urlacl url=http://+:" + httpPort +
        //                @"/ user=" + System.Environment.UserDomainName + @"\" + System.Environment.UserName);
        //        return null;
        //    }

        //    return client;
        //}

        private static void AttachDataContractResolver(ServiceEndpoint endpoint)
        {
            ContractDescription cd = endpoint.Contract;
            foreach (OperationDescription opdesc in cd.Operations)
            {
                var serializerBehavior = opdesc.Behaviors.Find<DataContractSerializerOperationBehavior>();
                if (serializerBehavior == null)
                {
                    serializerBehavior = new DataContractSerializerOperationBehavior(opdesc);
                    opdesc.Behaviors.Add(serializerBehavior);
                }
                serializerBehavior.DataContractResolver = new EngineContractResolver();
            }
        }

        //public static IEngine GetHttpCortexClient(string address)
        //{
        //    var binding = GetHttpBinding(SecurityMode.None);
        //    var endpoint = new EndpointAddress(address);
        //    var client = new CortexClient(binding, endpoint);
        //    return client;
        //    //return Connect<IEngine, CortexClient>(address);
        //}

        //public static IEngine GetPipeCortexClient(string address)
        //{
        //    var binding = GetPipeBinding(NetNamedPipeSecurityMode.None);
        //    var endpoint = new EndpointAddress(address);
        //    var client = new CortexClient(binding, endpoint);
        //    return client;
        //    //return Connect<IEngine, CortexClient>(address);
        //}

        public static WSHttpBinding GetHttpBinding(SecurityMode mode)
        {
            return new WSHttpBinding(mode)
            {
                OpenTimeout = OpenTimeout,
                CloseTimeout = CloseTimeout,
                ReceiveTimeout = ReceiveTimeout,
                SendTimeout = SendTimeout,
                MaxReceivedMessageSize = MaxReceivedMessageSize,
                ReaderQuotas =
                {
                    MaxArrayLength = MaxArrayLength,
                    MaxStringContentLength = MaxArrayLength,
                    MaxBytesPerRead = MaxArrayLength
                }
            };
        }

        public static WSDualHttpBinding GetDualHttpBinding(WSDualHttpSecurityMode mode)
        {
            return new WSDualHttpBinding(mode)
            {
                OpenTimeout = OpenTimeout,
                CloseTimeout = CloseTimeout,
                ReceiveTimeout = TimeSpan.MaxValue,
                SendTimeout = SendTimeout,
                MaxReceivedMessageSize = MaxReceivedMessageSize,
                ReaderQuotas =
                {
                    MaxArrayLength = MaxArrayLength,
                    MaxStringContentLength = MaxArrayLength,
                    MaxBytesPerRead = MaxArrayLength
                }
            };
        }

        public static NetTcpBinding GetTcpBinding(SecurityMode mode)
        {
            return new NetTcpBinding(mode)
            {
                OpenTimeout = OpenTimeout,
                CloseTimeout = CloseTimeout,
                ReceiveTimeout = ReceiveTimeout,
                SendTimeout = SendTimeout,
                MaxReceivedMessageSize = MaxReceivedMessageSize,
                ReaderQuotas =
                {
                    MaxArrayLength = MaxArrayLength,
                    MaxStringContentLength = MaxArrayLength,
                    MaxBytesPerRead = MaxArrayLength
                }
            };
        }

        public static NetNamedPipeBinding GetPipeBinding(NetNamedPipeSecurityMode mode)
        {
            return new NetNamedPipeBinding(mode)
            {
                OpenTimeout = OpenTimeout,
                CloseTimeout = CloseTimeout,
                ReceiveTimeout = ReceiveTimeout,
                SendTimeout = SendTimeout,
                MaxReceivedMessageSize = MaxReceivedMessageSize,
                ReaderQuotas =
                {
                    MaxArrayLength = MaxArrayLength,
                    MaxStringContentLength = MaxArrayLength,
                    MaxBytesPerRead = MaxArrayLength
                }
            };
        }


        /// <summary>
        /// Establishes a connection to a generic web-service defined by the two method arguments.
        /// </summary>
        /// <typeparam name="T">A client type</typeparam>
        /// <typeparam name="TK">The specific web service client for T</typeparam>
        /// <param name="address">The location of the web service. The uri endpoint.</param>
        /// <param name="username">The username</param>
        /// <param name="password">The password</param>
        /// <param name="securityMode">Security mode (Transport, Message, None, Transport with Message)</param>
        /// <param name="credentialType">Only Username, Basic and None are supported</param>
        /// <param name="disableValidation">A debug flag to disable certificate validation</param>
        /// <returns></returns>
        public static TK Connect<T, TK>(
            string address,
            string username = "",
            string password = "",
            SecurityMode securityMode = SecurityMode.None,
            MessageCredentialType credentialType = MessageCredentialType.None,
            bool disableValidation = true)
            where TK : ClientBase<T>
            where T : class
        {
            // Try to open the connection
            TK target = null;
            try
            {
                // Disable certficate validation
                if (disableValidation)
                {
                    ServicePointManager.ServerCertificateValidationCallback += RemoteCertificateValidate;
                }

                // Create a new client
                target = CreateClientHttpBinding<T, TK>(address, securityMode, credentialType);

                // Set username & password
                if (credentialType == MessageCredentialType.UserName)
                {
                    target.ClientCredentials.UserName.UserName = username;
                    target.ClientCredentials.UserName.Password = password;                    
                }

                // Associate with faulted event
                // TODO: Get this working
                //((ICommunicationObject)target).Faulted +=
                //    (sender, e) => FaultedEventHandler<TK, T>(address, username, password, sender, e); 
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            return target;
        }

        private static TK CreateClientHttpBinding<T, TK>(
            string endPointAddress,
            SecurityMode securityMode,
            MessageCredentialType credentialType)
            where TK : ClientBase<T>
            where T : class
        {
            // Create binding
            var binding = new WSHttpBinding(securityMode);
            binding.Security.Message.ClientCredentialType = credentialType;
            var endPoint = new EndpointAddress(endPointAddress);

            // Set preferences
            var settings = ConfigurationManager.AppSettings;
            var receiveTimeout = settings.Get("receiveTimeout");
            var sendTimeout = settings.Get("sendTimeout");
            var maxReceivedMessageSize = settings["maxReceivedMessageSize"];
            var maxArrayLength = settings["maxArrayLength"];
            binding.ReceiveTimeout = receiveTimeout == null ? ReceiveTimeout : TimeSpan.Parse(receiveTimeout);
            binding.SendTimeout = sendTimeout == null ? SendTimeout : TimeSpan.Parse(sendTimeout);
            binding.MaxReceivedMessageSize = maxReceivedMessageSize == null 
                ? MaxReceivedMessageSize
                : Convert.ToInt32(maxReceivedMessageSize);
            binding.ReaderQuotas.MaxArrayLength = maxArrayLength == null 
                ? MaxArrayLength 
                : Convert.ToInt32(maxArrayLength);
            binding.ReaderQuotas.MaxStringContentLength = maxArrayLength == null
                ? MaxArrayLength
                : Convert.ToInt32(maxArrayLength);
            binding.ReaderQuotas.MaxBytesPerRead = maxArrayLength == null
                ? MaxArrayLength
                : Convert.ToInt32(maxArrayLength);

            // Create instance
            var result = typeof(TK).GetConstructor(new[] { binding.GetType(), endPoint.GetType() });
            var client = (TK)result.Invoke(BindingFlags.CreateInstance, null, new object[] { binding, endPoint }, null);
            return client;
        }

        /// <summary>
        /// Circumvents certificate validation
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="certificate"></param>
        /// <param name="chain"></param>
        /// <param name="sslpolicyerrors"></param>
        /// <returns></returns>
        private static bool RemoteCertificateValidate(
            object sender,
            X509Certificate certificate,
            X509Chain chain,
            SslPolicyErrors sslpolicyerrors)
        {
            return true;
        }

        #endregion

    }
}
